/*
 *
 *  * Unidata Platform
 *  * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *  *
 *  * Commercial License
 *  * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *  *
 *  * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 *  * For clarification or additional options, please contact: info@unidata-platform.com
 *  * -------
 *  * Disclaimer:
 *  * -------
 *  * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 *  * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 *  * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 *  * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 *  * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 *
 */

package org.unidata.mdm.data.service.segments.records.draft;

import java.util.Objects;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.keys.ExternalId;
import org.unidata.mdm.core.type.keys.LSN;
import org.unidata.mdm.core.type.timeline.TimeInterval;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.context.GetRecordTimelineRequestContext;
import org.unidata.mdm.data.context.RecordIdentityContext;
import org.unidata.mdm.data.dto.GetTimelineResult;
import org.unidata.mdm.data.exception.DataExceptionIds;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.impl.CommonRecordsComponent;
import org.unidata.mdm.data.service.impl.RecordComposerComponent;
import org.unidata.mdm.data.service.segments.RecordDraftTimelineSupport;
import org.unidata.mdm.data.type.data.OriginRecord;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.draft.context.DraftGetContext;
import org.unidata.mdm.draft.dto.DraftGetResult;
import org.unidata.mdm.draft.exception.DraftProcessingException;
import org.unidata.mdm.draft.type.Draft;
import org.unidata.mdm.draft.type.DraftPayloadResponse;
import org.unidata.mdm.draft.type.Edition;
import org.unidata.mdm.system.type.pipeline.Finish;
import org.unidata.mdm.system.type.pipeline.Start;

/**
 * @author Alexey Tsarapkin
 */
@Component(RecordDraftGetFinishExecutor.SEGMENT_ID)
public class RecordDraftGetFinishExecutor extends Finish<DraftGetContext, DraftGetResult>
    implements RecordDraftTimelineSupport {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RECORD_DRAFT_GET_FINISH]";
    /**
     * This segment description.
     */
    private static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".record.draft.get.finish.description";
    /**
     * CRC.
     */
    @Autowired
    private CommonRecordsComponent commonRecordsComponent;
    /**
     * RCC.
     */
    @Autowired
    private RecordComposerComponent recordComposerComponent;
    /**
     * Constructor.
     */
    public RecordDraftGetFinishExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION, DraftGetResult.class);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DraftGetResult finish(DraftGetContext ctx) {

        DraftGetResult result = new DraftGetResult();

        Draft draft = ctx.currentDraft();
        Edition edition = ctx.currentEdition();

        result.setDraft(draft);
        result.setPayload(payload(draft, edition, ctx.getPayload()));

        return result;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(Start<?, ?> start) {
        return DraftGetContext.class.isAssignableFrom(start.getInputTypeClass());
    }

    private DraftPayloadResponse payload(Draft draft, Edition edition, GetRecordTimelineRequestContext payload) {

        Timeline<OriginRecord> timeline = load(draft, edition, payload);

        // 1. Try to build the TL
        if (Objects.isNull(timeline)) {
            return null;
        }

        // 2. Possibly reduce TL by given boundaries.
        if (Objects.nonNull(payload)) {
            if (Objects.nonNull(payload.getForDatesFrame())) {
                timeline = timeline.reduceBy(payload.getForDatesFrame().getLeft(), payload.getForDatesFrame().getRight());
            } else if (Objects.nonNull(payload.getForDate())) {
                timeline = timeline.reduceAsOf(payload.getForDate());
            }
        }

        // 3. Calc suff, if not disabled
        boolean skipCalculations = Objects.nonNull(payload) && payload.isSkipCalculations();
        boolean skipEtalon = Objects.nonNull(payload) && !payload.isFetchData();
        if (!skipCalculations) {

            RecordKeys keys = timeline.getKeys();
            for (TimeInterval<OriginRecord> ti : timeline) {

                ti.setActive(recordComposerComponent.isActive(ti.toList()));
                if (!skipEtalon) {
                    recordComposerComponent.toEtalon(keys, ti);
                }
            }
        }

        return new GetTimelineResult<>(timeline, draft.getDraftId());
    }

    private Timeline<OriginRecord> load(Draft draft, Edition edition, GetRecordTimelineRequestContext payload) {

        // 1. Load current approved.
        Timeline<OriginRecord> timeline;

        // Existing record
        if (StringUtils.isNotBlank(draft.getSubjectId())) {

            if (!draft.hasEditions()) {

                GetRecordTimelineRequestContext repackage = GetRecordTimelineRequestContext.builder(payload)
                    .draftId(null)
                    .build();

                repackage.keys(payload.keys());
                timeline = commonRecordsComponent.loadTimeline(repackage);
            } else {
                timeline = timeline(draft, edition);
            }
        // New one
        } else  {

            // 1.1. A new record has no editions - nothing to load
            if (!draft.hasEditions()) {
                timeline = null;
            } else {
                timeline = timeline(draft, edition);
            }
        }

        // Check
        verify(draft, payload, timeline);

        return timeline;
    }

    private void verify(Draft draft, GetRecordTimelineRequestContext payload, @Nullable Timeline<OriginRecord> timeline) {

        // Nothing to check.
        if (Objects.isNull(timeline) || Objects.isNull(timeline.getKeys())) {
            return;
        }

        RecordKeys keys = timeline.getKeys();
        if (StringUtils.isNotBlank(draft.getSubjectId()) && !draft.hasEditions()) {
            throwIfSubjectsDontMatch(draft.getSubjectId(), keys.getEtalonKey().getId());
        }

        throwIfExternalIdDontMatch(payload, keys);
        throwIfEtalonIdDontMatch(payload, keys);
        throwIfLsnDontMatch(payload, keys);
    }

    private void throwIfSubjectsDontMatch(String subjectId, String etalonId) {
        if (!StringUtils.equals(subjectId, etalonId)) {
            throw new DraftProcessingException("Supplied context id [{}] doesn't match with expected [{}].",
                    DataExceptionIds.EX_DATA_RECORD_DRAFT_SUBJECTS_DONT_MATCH, etalonId, subjectId);
        }
    }

    private void throwIfExternalIdDontMatch(RecordIdentityContext payload, RecordKeys keys) {
        if (payload.isOriginExternalId() || payload.isEnrichmentKey()) {
            ExternalId extId = payload.getExternalIdAsObject();
            if (keys.findByExternalId(extId.getId(), extId.getEntityName(), extId.getSourceSystem()) == null) {
                throw new DraftProcessingException("Supplied context external id [{}] doesn't match with record's expected [{}].",
                        DataExceptionIds.EX_DATA_RECORD_DRAFT_EXT_IDS_DONT_MATCH, extId, keys.getOriginKey().toExternalId());
            }
        }
    }

    private void throwIfEtalonIdDontMatch(RecordIdentityContext payload, RecordKeys keys) {
        if (payload.isEtalonRecordKey()) {
            throwIfSubjectsDontMatch(keys.getEtalonKey().getId(), payload.getEtalonKey());
        }
    }

    private void throwIfLsnDontMatch(RecordIdentityContext payload, RecordKeys keys) {
        if (payload.isLsnKey()) {
            LSN supplied = payload.getLsnAsObject();
            LSN expected = keys.getLsnAsObject();
            if (!supplied.equals(expected)) {
                throw new DraftProcessingException("Supplied context LSN [{}] doesn't match with record's expected [{}].",
                        DataExceptionIds.EX_DATA_RECORD_DRAFT_LSNS_DONT_MATCH, supplied, expected);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CommonRecordsComponent commonRecordsComponent() {
        return commonRecordsComponent;
    }
}
